﻿Imports System.IO
Imports System.Text
Imports System.Collections.Specialized

Public Class fmMain

    Private Enum Versions
        NotSelected
        Version8
        Version9
        Version10
    End Enum

    Private ConvertTo As Versions

    Private Sub fmMain_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' if started via a shell extension
        If Environment.GetCommandLineArgs.Length = 2 Then
            tbSolutionFile.Text = Environment.GetCommandLineArgs(1)

            ' Let's make sure this is valid solution file
            Try
                LoadChoices(GetVersion(tbSolutionFile.Text))
            Catch ex As Exception
                MsgBox("Error reading the solution file" & vbCr & ex.Message, MsgBoxStyle.Critical, "Error")
            End Try
        End If
    End Sub

    '
    ' Browse for a solution file
    '
    Private Sub bnBrowse_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles bnBrowse.Click
        ofdMain.AddExtension = True
        ofdMain.Filter = "Microsoft Visual Studio Solution File (*.sln)|*.sln|All files (*.*)|*.*"
        If ofdMain.ShowDialog = Windows.Forms.DialogResult.OK Then
            tbSolutionFile.Text = ofdMain.FileName
            Try
                LoadChoices(GetVersion(tbSolutionFile.Text))
            Catch ex As Exception
                MsgBox("Error reading the solution file" & vbCr & ex.Message, MsgBoxStyle.Critical, "Error")
            End Try
        End If
    End Sub

    '
    ' Load the appropriate choices for conversion
    '
    Private Sub LoadChoices(ByVal version As Double)
        lbVersion.Items.Clear()
        Select Case version
            Case 9.0
                lbMessage.Text = "Convert existing VS2005 (v8.0) solution"
                lbVersion.Items.Add("Visual Studio 2008 (v9.0)")
                lbVersion.Items.Add("Visual Studio 2010 (v10.0)")
            Case 10.0
                lbMessage.Text = "Convert existing VS2008 (v9.0) solution"
                lbVersion.Items.Add("Visual Studio 2005 (v8.0)")
                lbVersion.Items.Add("Visual Studio 2010 (v10.0)")
            Case 11.0
                lbMessage.Text = "Convert existing VS2010 (v10.0) solution"
                lbVersion.Items.Add("Visual Studio 2005 (v8.0)")
                lbVersion.Items.Add("Visual Studio 2008 (v9.0)")
            Case Else
                ConvertTo = Versions.NotSelected
                Exit Sub
        End Select

        ' clear the selection
        ConvertTo = Versions.NotSelected

        ' User setttings to control autoselecting the version
        If My.Settings.AutoSelectNewer Then
            lbVersion.SelectedItem = lbVersion.Items(1)
        End If
        If My.Settings.AutoSelectOlder Then
            lbVersion.SelectedItem = lbVersion.Items(0)
        End If
    End Sub

    '
    ' Get the Solution file "format version".  Also verifies we have a valid solution file
    '
    Private Function GetVersion(ByVal FileName As String) As Double
        Dim fs As New FileStream(tbSolutionFile.Text, FileMode.Open)
        Dim sr As New StreamReader(fs)
        Dim buf As String
        Dim linecount As Integer
        Dim ans As Double = 0.0

        Do While Not sr.EndOfStream
            buf = sr.ReadLine

            ' is this a valid header?
            If buf.StartsWith("Microsoft Visual Studio Solution File, Format Version") Then
                ans = Double.Parse(buf.Substring(54), Globalization.CultureInfo.GetCultureInfo("en-US"))
                Exit Do
            End If

            linecount += 1
            ' A valid solution should have a header at the 1st or 2nd line
            If linecount > 2 Then
                Exit Do
            End If
        Loop

        sr.Close()
        fs.Close()

        Select Case ans
            Case 0
                Throw New ApplicationException("This doesn't appear to be a valid solution file")
            Case 7.0
                Throw New ApplicationException("Conversion of VS.Net (v7.0) solutions is not supported")
            Case 8.0
                Throw New ApplicationException("Conversion of VS2003 (v7.1) solutions is not supported")
        End Select

        Return ans
    End Function

    '
    ' Select the version you are converting to
    '
    Private Sub lbVersion_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lbVersion.SelectedIndexChanged
        Dim v As Double
        Dim ending As Integer

        ' chop off any previous ending of the message
        ending = lbMessage.Text.IndexOf("solution")
        lbMessage.Text = lbMessage.Text.Substring(0, ending + 8)

        v = Double.Parse(lbVersion.SelectedItem.ToString.Substring(21, 3), Globalization.CultureInfo.GetCultureInfo("en-US"))
        Select Case v
            Case 8.0
                ConvertTo = Versions.Version8
                lbMessage.Text &= " to VS2005 (v8.0)"
            Case 9.0
                ConvertTo = Versions.Version9
                lbMessage.Text &= " to VS2008 (v9.0)"
            Case 10.0
                ConvertTo = Versions.Version10
                lbMessage.Text &= " to VS2010 (v10.0)"
            Case Else
                ' not likely
                MsgBox("Not a valid choice", MsgBoxStyle.Exclamation, "Internal Error")
        End Select
    End Sub

    '
    ' Perform the conversion
    '
    Private Sub bnConvert_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles bnConvert.Click
        Dim fs As FileStream
        Dim sr As StreamReader
        Dim sw As StreamWriter
        Dim br As BinaryReader
        Dim bw As BinaryWriter
        Dim sc As StringCollection
        Dim buf As String
        Dim bom() As Byte
        Dim ExistingVersion As Double
        Dim ProjCount As Integer

        ' A quick sanity check
        If tbSolutionFile.Text = "" Then
            MsgBox("Please enter a valid Visual Studio solution file", MsgBoxStyle.Exclamation, "Oops")
            Exit Sub
        End If

        If ConvertTo = Versions.NotSelected Then
            MsgBox("Please enter a select a version to convert to", MsgBoxStyle.Exclamation, "Oops")
            Exit Sub
        End If

        ' Let's make sure this is valid solution file
        Try
            ExistingVersion = GetVersion(tbSolutionFile.Text)
        Catch ex As Exception
            MsgBox("Error reading the solution file" & vbCr & ex.Message, MsgBoxStyle.Critical, "Error")
            Exit Sub
        End Try

        ' Did you ask to make a backup copy?
        If cbBackup.Checked Then
            Try
                MakeBackup(tbSolutionFile.Text, ExistingVersion)
            Catch ex As Exception
                MsgBox("Could not create backup file" & vbCr & ex.Message, MsgBoxStyle.Critical, "Error")
                Exit Sub
            End Try
        End If

        ' First we read the solution file and build a list of 
        ' project files that we find inside.  We need both a binary
        ' reader and stream reader.
        fs = New FileStream(tbSolutionFile.Text, FileMode.Open)
        sr = New StreamReader(fs)
        br = New BinaryReader(fs)

        ' let's read Unicode Byte Order Mark (with CRLF)
        bom = br.ReadBytes(5)

        ' if we don't have a BOM, we create a default one
        If bom(0) <> &HEF Then
            bom(0) = &HEF
            bom(1) = &HBB
            bom(2) = &HBF
            bom(3) = &HD
            bom(4) = &HA

            ' rewind the streamreaders
            fs.Seek(0, SeekOrigin.Begin)
        End If

        sc = New StringCollection
        Do While sr.Peek() >= 0
            buf = sr.ReadLine

            ' no need for any fancy parsing routines
            If buf.StartsWith("Microsoft Visual Studio Solution File, Format Version") Then
                ' Yes, I know, this looks counter intuitive... but format version numbers
                ' do not match the Visual Studio version numbers.
                Select Case ConvertTo
                    Case Versions.Version8
                        sc.Add("Microsoft Visual Studio Solution File, Format Version 9.00")
                    Case Versions.Version9
                        sc.Add("Microsoft Visual Studio Solution File, Format Version 10.00")
                    Case Versions.Version10
                        sc.Add("Microsoft Visual Studio Solution File, Format Version 11.00")
                End Select
                Continue Do
            End If

            If buf.StartsWith("# Visual") Then
                Select Case ConvertTo
                    Case Versions.Version8
                        sc.Add("# Visual Studio 2005")
                    Case Versions.Version9
                        sc.Add("# Visual Studio 2008")
                    Case Versions.Version10
                        sc.Add("# Visual Studio 2010")
                End Select
                Continue Do
            End If

            sc.Add(buf)

            ' parse the project files
            If buf.StartsWith("Project(") Then
                Dim ProjParts(), ProjFile, ProjExt As String

                ProjParts = buf.Split(","c)
                If ProjParts.Length = 3 Then
                    ProjFile = Path.GetDirectoryName(tbSolutionFile.Text) & "\" & ProjParts(1).Trim.Trim(Chr(34))
                    ' we only support VB, C#, and C/C++ project types so that means 
                    ' we do not attempt to convert Deployment Projects (*.vdproj)
                    Try
                        ProjExt = Path.GetExtension(ProjFile)
                        If ProjExt = ".vbproj" Or ProjExt = ".csproj" Then
                            ' convert the VB/C# project files
                            If ConvertProject(ProjFile, ExistingVersion) Then
                                ProjCount += 1
                            End If
                        ElseIf ProjExt = ".vcproj" Or ProjExt = ".vcxproj" Then
                            ' convert the C/C++project files
                            If ConvertVCProject(ProjFile, ExistingVersion) Then
                                ProjCount += 1
                            End If
                        End If
                    Catch ex As Exception
                        MsgBox("Could not convert project file" & vbCr & ex.Message, MsgBoxStyle.Critical, "Error")
                        ' TODO:  Perform a "rollback" on those files that
                        ' got converted so far?
                        Exit Sub
                    End Try
                End If
            End If
        Loop
        fs.Close()

        ' OK now it's time to save the converted Solution file
        fs = New FileStream(tbSolutionFile.Text, FileMode.Open)
        sw = New StreamWriter(fs)
        bw = New BinaryWriter(fs)

        ' write the BOM bytes (using the BinaryWriter)
        bw.Write(bom)
        bw.Flush()

        ' write the remaining text (using the StreamWriter)
        For Each buf In sc
            sw.WriteLine(buf)
        Next
        sw.Flush()
        fs.Close()

        ' Tell 'em what we did
        MsgBox("Converted 1 solution and " & ProjCount & " project file" & IIf(ProjCount > 1, "s", "").ToString, MsgBoxStyle.Information, "Sucess")

        ' reload the version (in case you want to hit the convert button a 2nd time)
        LoadChoices(GetVersion(tbSolutionFile.Text))

        ' if started from a shell extension, then close when we're done
        If Environment.GetCommandLineArgs.Length = 2 Then
            Me.Close()
        End If
    End Sub

    '
    ' Make a backup copy of the file
    '
    Private Sub MakeBackup(ByVal FileName As String, ByVal ExistingVersion As Double)
        Dim Backup As String = ""

        ' remember, ExistingVersion is the file version #, and not the VS version #
        Select Case ExistingVersion
            Case 9.0
                Backup = Path.Combine(Path.GetDirectoryName(FileName), Path.GetFileNameWithoutExtension(FileName)) & ".v8" & Path.GetExtension(FileName)
            Case 10.0
                Backup = Path.Combine(Path.GetDirectoryName(FileName), Path.GetFileNameWithoutExtension(FileName)) & ".v9" & Path.GetExtension(FileName)
            Case 11.0
                Backup = Path.Combine(Path.GetDirectoryName(FileName), Path.GetFileNameWithoutExtension(FileName)) & ".v10" & Path.GetExtension(FileName)
            Case Else
                ' not likely
                MsgBox("Not a supported version", MsgBoxStyle.Exclamation, "Internal Error")
        End Select

        File.Copy(FileName, Backup, True)
    End Sub

    '
    ' Convert the C/C++ project files
    '
    Private Function ConvertVCProject(ByVal ProjFile As String, ByVal ExistingVersion As Double) As Boolean
        Dim xd As Xml.XmlDocument
        Dim xn, xtemp As Xml.XmlNode

        If ExistingVersion = 11.0 Or ConvertTo = Versions.Version10 Then
            ' There is a completely different project file format, that I haven't
            ' had the time to work out.
            Throw New ApplicationException("Converting To/From VS2010 C/C++ project is currently not supported")
        End If

        ' Did you ask to make a backup copy?
        If cbBackup.Checked Then
            MakeBackup(ProjFile, ExistingVersion)
        End If

        xd = New Xml.XmlDocument
        xd.Load(ProjFile)

        ' let's do a quick sanity check to make sure we've got the
        ' version that we're expecting.
        xn = xd.SelectSingleNode("VisualStudioProject")
        If xn IsNot Nothing Then
            xtemp = xn.Attributes("Version")
            If xtemp IsNot Nothing Then
                ' converting to VS200x, but project is already at VS200x
                If ExistingVersion = Double.Parse(xtemp.InnerText, Globalization.CultureInfo.GetCultureInfo("en-US")) Then
                    ' exit quietly
                    Return False
                End If

                Select Case ConvertTo
                    Case Versions.Version8
                        xtemp.InnerText = "8.00"
                    Case Versions.Version9
                        xtemp.InnerText = "9.00"
                End Select
            End If
        Else
            ' no such node?  That's bad, very bad...
            Throw New ApplicationException("Invalid project file")
        End If

        ' now we need to add/remove the new MinFrameworkVersion attribute
        ' of the AssembyReference
        For Each xns As Xml.XmlNode In xd.SelectNodes("/VisualStudioProject/References/AssemblyReference")
            If ConvertTo = Versions.Version9 Then
                ' add the attribute
                xtemp = xd.CreateAttribute("MinFrameworkVersion")
                xtemp.Value = "131072"
                xns.Attributes.Append(CType(xtemp, Xml.XmlAttribute))
            Else
                ' remove the attribute
                xns.Attributes.Remove(xns.Attributes("MinFrameworkVersion"))
            End If
        Next

        xd.Save(ProjFile)
        Return True
    End Function

    '
    ' Convert the VB and C# Project file
    '
    Private Function ConvertProject(ByVal ProjFile As String, ByVal ExistingVersion As Double) As Boolean
        Dim xd As Xml.XmlDocument
        Dim xnsm As Xml.XmlNamespaceManager
        Dim xn, xtemp As Xml.XmlNode
        Dim temp As String

        ' Did you ask to make a backup copy?
        If cbBackup.Checked Then
            MakeBackup(ProjFile, ExistingVersion)
        End If

        xd = New Xml.XmlDocument
        xd.Load(ProjFile)

        xnsm = New Xml.XmlNamespaceManager(New Xml.NameTable())
        xnsm.AddNamespace("prj", "http://schemas.microsoft.com/developer/msbuild/2003")

        ' let's do a quick sanity check to make sure we've got the
        ' version that we're expecting.  Hey, it happens
        xn = xd.SelectSingleNode("/prj:Project", xnsm)
        If xn IsNot Nothing Then
            xtemp = xn.Attributes("ToolsVersion")
            If xtemp IsNot Nothing Then
                ' converting to VS2008, but project is already at VS2008
                If ConvertTo = Versions.Version9 And xtemp.InnerText = "3.5" Then
                    ' exit quietly
                    Return False
                End If
                ' converting to VS2010, but project is already at VS2010
                If ConvertTo = Versions.Version10 And xtemp.InnerText = "4.0" Then
                    ' exit quietly
                    Return False
                End If
            Else
                ' If converting to VS2005, but project is already at VS2005
                If ConvertTo = Versions.Version8 Then
                    ' exit quietly
                    Return False
                End If
            End If
        Else
            ' no such node?  That's bad, very bad...
            Throw New ApplicationException("Invalid project file")
        End If

        ' the ToolsVersion attribute (we already have selected that node)
        xn.Attributes.Remove(xn.Attributes("ToolsVersion"))
        Select Case ConvertTo
            Case Versions.Version8
                ' it gets removed
            Case Versions.Version9
                ' add the attribute
                xtemp = xd.CreateAttribute("ToolsVersion")
                xtemp.Value = "3.5"
                xn.Attributes.Append(CType(xtemp, Xml.XmlAttribute))
            Case Versions.Version10
                ' add the attribute
                xtemp = xd.CreateAttribute("ToolsVersion")
                xtemp.Value = "4.0"
                xn.Attributes.Append(CType(xtemp, Xml.XmlAttribute))
        End Select

        ' the ProjectVersion element
        xn = xd.SelectSingleNode("/prj:Project/prj:PropertyGroup/prj:ProductVersion", xnsm)
        If xn IsNot Nothing Then
            Select Case ConvertTo
                Case Versions.Version8
                    xn.InnerText = My.Settings.VS2005_Version
                Case Versions.Version9
                    xn.InnerText = My.Settings.VS2008_Version
                Case Versions.Version10
                    ' not used... strange
                    xn.InnerText = ""
            End Select
        End If

        ' the OldToolsVersion element in the first PropertyGoup
        xn = xd.SelectSingleNode("/prj:Project/prj:PropertyGroup", xnsm)
        xtemp = xd.SelectSingleNode("/prj:Project/prj:PropertyGroup/prj:OldToolsVersion", xnsm)
        If xtemp IsNot Nothing Then
            xn.RemoveChild(xtemp)
        End If
        Select Case ConvertTo
            Case Versions.Version8
                ' it gets removed
            Case Versions.Version9
                ' add a new element
                ' Note: this doesn't appear to be added in every project type, but I bet it's harmless
                xtemp = xd.CreateElement("OldToolsVersion", xnsm.LookupNamespace("prj"))
                xtemp.AppendChild(xd.CreateTextNode("2.0"))
                xn.AppendChild(xtemp)
            Case Versions.Version10
                ' add a new element
                ' Note: this doesn't appear to be added in every project type, but I bet it's harmless
                xtemp = xd.CreateElement("OldToolsVersion", xnsm.LookupNamespace("prj"))
                xtemp.AppendChild(xd.CreateTextNode("3.5"))
                xn.AppendChild(xtemp)
        End Select

        ' remove/tweak the optional TargetFrameworkVersion element
        xn = xd.SelectSingleNode("/prj:Project/prj:PropertyGroup", xnsm)
        xtemp = xd.SelectSingleNode("/prj:Project/prj:PropertyGroup/prj:TargetFrameworkVersion", xnsm)
        If xtemp IsNot Nothing Then
            xn.RemoveChild(xtemp)
        End If
        Select Case ConvertTo
            Case Versions.Version8
                ' it gets removed
            Case Versions.Version9
                xtemp = xd.CreateElement("TargetFrameworkVersion", xnsm.LookupNamespace("prj"))
                xtemp.AppendChild(xd.CreateTextNode("v3.5"))
                xn.AppendChild(xtemp)
            Case Versions.Version10
                xtemp = xd.CreateElement("TargetFrameworkVersion", xnsm.LookupNamespace("prj"))
                xtemp.AppendChild(xd.CreateTextNode("v4.0"))
                xn.AppendChild(xtemp)
        End Select

        ' the optional BootStrapper elements.  I only remove the inappropriate values, I
        ' do not attempt to add the newer values
        Select Case ConvertTo
            Case Versions.Version8
                ' alter the value for the ProductName element using the older framework tag
                xn = xd.SelectSingleNode("/prj:Project/prj:ItemGroup/prj:BootstrapperPackage[@Include=""Microsoft.Net.Framework.2.0""]", xnsm)
                If xn IsNot Nothing Then
                    xtemp = xn.SelectSingleNode("prj:ProductName", xnsm)
                    xtemp.FirstChild.Value = ".NET Framework 2.0"
                End If
                ' remove the newer framework options
                xn = xd.SelectSingleNode("/prj:Project/prj:ItemGroup/prj:BootstrapperPackage[@Include=""Microsoft.Net.Framework.3.0""]", xnsm)
                If Not xn Is Nothing Then
                    xn.ParentNode.RemoveChild(xn)
                End If
                xn = xd.SelectSingleNode("/prj:Project/prj:ItemGroup/prj:BootstrapperPackage[@Include=""Microsoft.Net.Framework.3.5""]", xnsm)
                If Not xn Is Nothing Then
                    xn.ParentNode.RemoveChild(xn)
                End If
                xn = xd.SelectSingleNode("/prj:Project/prj:ItemGroup/prj:BootstrapperPackage[@Include=""Microsoft.Net.Client.3.5""]", xnsm)
                If Not xn Is Nothing Then
                    xn.ParentNode.RemoveChild(xn)
                End If
                xn = xd.SelectSingleNode("/prj:Project/prj:ItemGroup/prj:BootstrapperPackage[@Include=""Microsoft.Net.Framework.3.5.SP1""]", xnsm)
                If Not xn Is Nothing Then
                    xn.ParentNode.RemoveChild(xn)
                End If
                xn = xd.SelectSingleNode("/prj:Project/prj:ItemGroup/prj:BootstrapperPackage[@Include=""Microsoft.Windows.Installer.3.1""]", xnsm)
                If Not xn Is Nothing Then
                    xn.ParentNode.RemoveChild(xn)
                End If
            Case Versions.Version9
                ' alter the value for the ProjectName using the newer framework tag
                xn = xd.SelectSingleNode("/prj:Project/prj:ItemGroup/prj:BootstrapperPackage[@Include=""Microsoft.Net.Framework.2.0""]", xnsm)
                If xn IsNot Nothing Then
                    xtemp = xn.SelectSingleNode("prj:ProductName", xnsm)
                    xtemp.FirstChild.Value = ".NET Framework 2.0 %28x86%29"
                End If

                ' remove the newer framework options
                xn = xd.SelectSingleNode("/prj:Project/prj:ItemGroup/prj:BootstrapperPackage[@Include=""Microsoft.Net.Client.3.5""]", xnsm)
                If Not xn Is Nothing Then
                    xn.ParentNode.RemoveChild(xn)
                End If
                xn = xd.SelectSingleNode("/prj:Project/prj:ItemGroup/prj:BootstrapperPackage[@Include=""Microsoft.Net.Framework.3.5.SP1""]", xnsm)
                If Not xn Is Nothing Then
                    xn.ParentNode.RemoveChild(xn)
                End If
                xn = xd.SelectSingleNode("/prj:Project/prj:ItemGroup/prj:BootstrapperPackage[@Include=""Microsoft.Windows.Installer.3.1""]", xnsm)
                If Not xn Is Nothing Then
                    xn.ParentNode.RemoveChild(xn)
                End If
            Case Versions.Version10
                ' alter the value for the ProjectName using the newer framework tag
                xn = xd.SelectSingleNode("/prj:Project/prj:ItemGroup/prj:BootstrapperPackage[@Include=""Microsoft.Net.Framework.2.0""]", xnsm)
                If xn IsNot Nothing Then
                    xtemp = xn.SelectSingleNode("prj:ProductName", xnsm)
                    xtemp.FirstChild.Value = ".NET Framework 2.0 %28x86%29"
                End If
        End Select

        ' The MSBuildToolsPath vs MSBuildBinPath environmental variable.  Oddly enough a fully patched VS2005
        ' uses the newer MSBuildToolsPath.  So, this should only be required if you don't have VS2005 SP1 installed.
        ' However, I can't detect that, so we take the worst case scenario, and use the older version
        For Each xn In xd.SelectNodes("/prj:Project/prj:Import", xnsm)
            xtemp = xn.Attributes("Project")
            If xtemp IsNot Nothing Then
                temp = xn.Attributes("Project").Value
                If ConvertTo >= Versions.Version9 Then
                    ' convert it to the newer MSBuildToolsPath
                    If temp.Contains("MSBuildBinPath") Then
                        temp = temp.Replace("MSBuildBinPath", "MSBuildToolsPath")
                        xtemp.Value = temp
                    End If
                Else
                    ' convert it to the older MSBuildBinPath
                    If temp.Contains("MSBuildToolsPath") Then
                        temp = temp.Replace("MSBuildToolsPath", "MSBuildBinPath")
                        xtemp.Value = temp
                    End If
                End If
            End If
        Next

        xd.Save(ProjFile)
        Return True
    End Function

End Class
